from collections import defaultdict
from functools import lru_cache
from typing import List, Union

from flask import current_app, json, request
from flask_restx import abort
from sqlalchemy import and_

from app.common.rule import get_rule_tree
from app.common.view.base import BaseView
from app.models.roleModel import (
    FlagChoices,
    Permission,
    PresetRoleChoices,
    Role,
    RolePermission,
    UserRole,
)
from app.models.userModel import User
from configs.settings import flask_config
from core import ExtBlueprint, ExtRule
from tools import db
from utils.crypto import Crypt
from utils.utils import base_detail


class PermissionBase(BaseView):
    @property
    def rule_tree(self):
        return get_rule_tree()

    @lru_cache(maxsize=None)  # noqa
    def get_role_permission_tree(self):
        """获取完整权限树，该处数据由项目初始化权限命令生成，静态数据，可无限制缓存"""
        # 联合key已适应一个视图多个路由的情况，防止被覆盖
        # 同时对视图name分组去重，防止前端渲染时看起来有一样的权限节点
        permission_map = {
            (item.name, item.path): item
            for item in Permission.query.group_by(Permission.name).all()
        }
        # 获取所有内置不可编辑的权限ID
        disabled_node_ids = {
            val
            for val, in db.session.query(RolePermission.permission_id)
            .filter(RolePermission.allow_edit == 0)
            .all()
        }
        # 获取规则树
        return self.rule_tree.to_resources_dict(
            resources_map=permission_map, disabled_node_ids=disabled_node_ids
        )


class PermissionTreeView(PermissionBase):
    """获取完整权限树"""

    def get(self):
        tree = self.get_role_permission_tree()
        return {"code": 200, "data": tree}


class MenuView(PermissionBase):
    """获取用户动态菜单"""

    @staticmethod
    def filter_strategy(menu_info: list) -> list:
        """策略过滤"""
        # 暂时不需要增加策略控制，保留开关
        if not flask_config.USER_POLICY_FILTER:
            return menu_info

    @staticmethod
    def get_role_menu(role: Role) -> list:
        """获取当前角色允许访问的菜单"""
        # 获取该角色权限
        permission_list: Union[List[Permission], None] = Permission.query.join(
            RolePermission,
            and_(
                RolePermission.permission_id == Permission.id,
                RolePermission.role_id == role.id,
            ),
        ).all()

        menu_info = []

        # 扫描菜单
        def menu_scan(resources_list, _menu_info):
            for item in resources_list:
                _menu = {}
                is_menu = False
                if item in permission_list and item.is_menu:

                    is_rule = item.type == Permission.TypeChoices.rule.value
                    per: Union[ExtRule, ExtBlueprint] = (
                        next(current_app.url_map.iter_rules(item.name))
                        if is_rule
                        else current_app.blueprints[item.name]
                    )

                    # 适配前端同一个菜单不同角色前端路由不一样的情况
                    if isinstance(per.role_control.menu_url, dict):
                        menu_path = per.role_control.menu_url.get(role.name)
                    else:
                        menu_path = per.role_control.menu_url

                    _menu = {
                        "menu_name": per.role_control.menu_name,
                        "menu_path": menu_path,
                        "url_path": per.rule if is_rule else "",
                        "menu_order": per.role_control.menu_order,
                        "children": [],
                    }
                    _menu_info.append(_menu)
                    is_menu = True

                # 按权重进行排序，保证菜单展示顺序正确
                _menu_info.sort(key=lambda x: x["menu_order"], reverse=True)
                # 适配子路由层级比其作为菜单时层级要深的情况
                menu_scan(item.children, _menu["children"] if is_menu else _menu_info)

        # 获取一级蓝图及路由，依次扫描
        top_resources = [item for item in permission_list if item.parent_id is None]
        menu_scan(top_resources, menu_info)
        menu_info.sort(key=lambda x: x["menu_order"], reverse=True)

        return menu_info

    def post(self):
        current_role = request.cookies.get("role", None)
        if not current_role:
            abort(400, message="cookie信息缺失 role")
        if current_role not in [role.name for role in self.user.roles]:
            abort(400, message="cookie中角色信息错误，请重新登录")

        role: Union[Role, None] = (
            Role.query.join(
                UserRole,
                and_(UserRole.user_id == self.user.id, UserRole.role_id == Role.id),
            )
            .filter(Role.is_enabled == 1, Role.name == current_role)
            .first()
        )

        menu_info = {}
        if not role:
            return {"code": 200, "data": menu_info}

        menu_info = self.filter_strategy(self.get_role_menu(role))

        return {"code": 200, "data": menu_info}


class RoleSetView(PermissionBase):
    """获取角色对象"""

    def get(self):
        params = request.args
        page = params.get("page", type=int, default=1)
        page_size = params.get("pageSize", type=int, default=10)
        keyword = params.get("keyword", type=str)  # noqa
        _type = params.get("type", type=str, default=None)  # noqa

        _filter = [Role.name != PresetRoleChoices.admin]
        if keyword and keyword != PresetRoleChoices.admin:
            _filter.append(Role.name == keyword)

        if _type and _type == "all":
            page_size = 10000

        role_objs = (
            Role.query.filter(*_filter)
            .order_by(Role.id)
            .paginate(page, page_size, error_out=False)
        )

        role_info = [item.to_dict() for item in role_objs.items]

        data = {"role_info": role_info}
        base_detail(data, role_objs)
        return {"code": 200, "data": data}


class RolePermissionView(PermissionBase):
    """获取角色允许访问的路由及蓝图权限/用户绑定角色"""

    def get(self, role_id):
        """获取某个角色允许访问的蓝图、路由及请求方法"""
        permission_info = (
            db.session.query(Permission.id, Permission.type, RolePermission.flag)
            .join(
                RolePermission,
                and_(
                    RolePermission.permission_id == Permission.id,
                    RolePermission.role_id == role_id,
                ),
            )
            # 此处必须使用 ==
            # 适配前端组件，过滤权限树全部叶子节点，以便前端展示树形结构选中效果
            .filter(Permission.children is None)
            .all()
        )

        result = []

        for item in permission_info:
            resources_id, resources_type, supper_flag = item

            result.append(Crypt.encrypt(str(resources_id)))

            if resources_type == 1:
                supper_flag = int(supper_flag)
                for val in FlagChoices:
                    if supper_flag & val.value:
                        result.append(Crypt.encrypt(f"{resources_id}:{val}"))

        return {
            "code": 200,
            "data": result,
        }

    def post(self, role_id=None):

        kwargs = json.loads(request.data)
        if not role_id:
            abort(400, message="role_id 参数缺失")
        if "user_list" not in kwargs:
            abort(400, message="user_list 参数缺失")

        user_ids = set(kwargs["user_list"])
        supper_user_ids = {
            user.id for user in User.query.filter(User.is_superuser == 1).all()
        }
        modify_ids = user_ids - supper_user_ids

        with db.auto_commit():
            with db.session.begin(subtransactions=True):
                UserRole.query.filter(
                    UserRole.role_id == role_id,
                    UserRole.user_id.notin_(supper_user_ids),
                ).delete()
                db.session.flush()

                new_relationship = [
                    UserRole(user_id=_id, role_id=role_id) for _id in modify_ids
                ]
                db.session.bulk_save_objects(new_relationship)

        return {"code": 200, "data": ""}

    def modify_role_permission(
        self,
        permission_list,
        role_permission_query,
        role,
        permission_ids,
        ext_permission_map,
    ):
        """
        修改角色权限
        :param list permission_list: 预备赋予的权限树节点ID
        :param queryset role_permission_query: 角色已有权限过滤条件
        :param Role role: 待修改的角色
        :param set permission_ids: 待赋予该角色的权限ID
        :param dict ext_permission_map: 支持的url请求方法
        """
        if not permission_list:
            current_app.logger.warning(
                f"{self.user.account_name} 预备清空 {role.name} 分配的权限"
            )
            role_permission_query.delete()
            return

        allow_edit_bind_map = {}
        for item in role_permission_query.all():
            if item.allow_edit:
                allow_edit_bind_map.update({item.permission_id: item})
            else:
                if item.permission_id in permission_ids:
                    permission_ids.remove(item.permission_id)
                    ext_permission_map.pop(item.permission_id, None)

        allow_edit_ids: set = set(allow_edit_bind_map.keys())

        unbind_permission_ids: set = allow_edit_ids - permission_ids
        if unbind_permission_ids:
            role_permission_query.filter(
                RolePermission.permission_id.in_(unbind_permission_ids)
            ).delete()
            db.session.flush()

        update_bind_permission_ids: set = allow_edit_ids & set(
            ext_permission_map.keys()
        )
        for _id in update_bind_permission_ids:
            _role_permission: RolePermission = allow_edit_bind_map[_id]

            modify_flag = sum(ext_permission_map[_id])
            if _role_permission.flag != modify_flag:
                _role_permission.flag = modify_flag
                db.session.add(_role_permission)
        db.session.flush()

        new_relationship = (
            permission_ids - allow_edit_ids
        ) - update_bind_permission_ids
        new_relationship_obj = [
            RolePermission(
                role_id=role.id,
                permission_id=_id,
                flag=sum(ext_permission_map[_id])
                if ext_permission_map.get(_id)
                else None,
            )
            for _id in new_relationship
        ]
        db.session.bulk_save_objects(new_relationship_obj)
        db.session.flush()

    def put(self, role_id):

        kwargs = request.json
        if "permission_list" not in kwargs:
            abort(400, message="节点列表参数缺失")
        role = Role.query.filter(Role.id == role_id).first()
        if not role:
            abort(400, message="角色不存在，请刷新角色管理页面")

        permission_list = kwargs["permission_list"]

        permission_ids = set()
        ext_permission_map = defaultdict(list)
        for val in permission_list:
            salt_id = Crypt.decrypt(val)
            if salt_id.isdigit():
                permission_ids.add(int(salt_id))
            else:
                db_id, ext_flag = salt_id.split(":")
                permission_ids.add(int(db_id))
                ext_permission_map[int(db_id)].append(int(ext_flag))

        # 由于展示时对同一个rule多个url情况进行过滤，这里需要根据视图名称匹配全部节点
        permission_list = Permission.query.filter(
            Permission.name.in_(
                db.session.query(Permission.name).filter(
                    Permission.id.in_(permission_ids)
                )
            )
        ).all()

        role_permission_query = RolePermission.query.filter(
            RolePermission.role_id == role_id
        )

        with db.auto_commit():
            with db.session.begin(subtransactions=True):
                try:
                    self.modify_role_permission(
                        permission_list,
                        role_permission_query,
                        role,
                        permission_ids,
                        ext_permission_map,
                    )
                except Exception as e:
                    db.session.rollback()
                    current_app.logger.error("保存角色权限树失败", exc_info=e)
                    abort(500, message="保存角色权限树失败")

        return {"code": 200, "data": ""}
